<?php

/**
 * A generic class to describe a physical address or location
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2007-2009, Philippe Jausions / 11abacus
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   - Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of the 11abacus nor the names of its contributors may
 *     be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @package   Structures_Location
 * @category  Structures
 * @author    Philippe Jausions <Philippe.Jausions@11abacus.com>
 * @copyright 2007-2009 by Philippe Jausions / 11abacus
 * @license   http://www.opensource.net/licenses/bsd-license.php New BSD
 */

/**
 * Requires dependencies
 */
require_once 'Structures/LocationInterface.php';
require_once 'Structures/Location/GeodeticInterface.php';
require_once 'Structures/Location/Exception.php';
require_once 'Structures/Location/ParserInterface.php';
require_once 'I18Nv2/Country.php';

/**
 * A generic class to describe a physical address or location
 *
 * @category Structures
 * @package  Structures_Location
 * @author   Philippe Jausions <Philippe.Jausions@11abacus.com>
 */
class Structures_Location implements Structures_LocationInterface
{
    /**
     * Address part constants
     */
    const PART_LOCALITY = 'locality';
    const PART_POSTALCODE = 'postalCode';
    const PART_POSTALREGION = 'postalRegion';
    const PART_POSTALSUBREGION = 'postalSubRegion';
    const PART_COUNTRY = 'country';
    const PART_POSTALOFFICE = 'postalOffice';
    
    /**
     * Street address
     *
     * @var string
     */
    protected $streetAddress;

    /**
     * Apartment, building, unit information
     *
     * @var string
     */
    protected $secondaryUnit;

    /**
     * City/locality
     *
     * @var string
     */
    protected $locality;

    /**
     * Country code
     *
     * @var string 2-letter country code
     */
    protected $countryCode;

    /**
     * Region code (state, province, region, ...)
     *
     * @var string
     */
    protected $regionCode;

    /**
     * Sub-region code (county, departement, ...)
     *
     * @var string
     */
    protected $subRegion;

    /**
     * Geodetic location
     *
     * @var Structures_Location_GeodeticInterface
     */
    protected $geodetic;

    /**
     * A locale-dependent parser instance 
     *
     * @var Structures_Location_ParserInterface
     */
    public $parser;

    /**
     * Class constructor
     *
     * @param array $values Array indexes processed:
     * <ul>
     *  <li>countryCode (string)</li>
     *  <li>locality (string)</li>
     *  <li>geodetic (Structures_Location_GeodeticInterface)</li>
     *  <li>regionCode (string)</li>
     *  <li>secondaryUnit (string)<li>
     *  <li>streetAddress (string)</li>
     *  <li>subRegion (string)</li>
     * </ul>
     */
    public function __construct(array $values = array())
    {
        foreach ($values as $name => $value) {
            switch ($name) {
            case 'streetAddress':
            case 'secondaryUnit':
            case 'locality':
            case 'countryCode':
            case 'regionCode':
            case 'subRegion':
            case 'geodetic':
                $method = 'set'.ucfirst($name);
                $this->{$method}($value);
                break;
            }
        }
    }

    /**
     * Clone properties
     *
     * @return void
     * @access public
     */
    public function __clone()
    {
        if (isset($this->geodetic)) {
            $this->geodetic = clone $this->geodetic;
        }
    }

    /**
     * Returns the "street" address
     * 
     * "Street" is used loosely to represent any thoroughfare, place, etc...
     * A street address is usually composed of:
     * <ul>
     *  <li>thoroughfare type (i.e. avenue, boulevard, plaza, ...)</li>
     *  <li>thoroughfare number (not necessarily digits only)</li>
     *  <li>thoroughfare name</li>
     * </ul>
     * The order of these elements depends on the locale of the country.
     * Additional location information such as apartment, building, dock, unit
     * and so on should be accessed with getSecondaryUnit().
     * 
     * @return string|NULL "Street" address or NULL if none is set
     */
    public function getStreetAddress()
    {
        return $this->streetAddress;
    }

    /**
     * Returns the address line for apartment, suite, lot, unit, etc
     * 
     * @return string|NULL Secondary "unit" information or NULL if none is set
     */
    public function getSecondaryUnit()
    {
        return $this->secondaryUnit;
    }

    /**
     * Returns the locality
     * 
     * This is often useful for rural locations where mail is delivered to the
     * closest town. Postal office and locality are often used interchangeably
     * but could be different.
     *
     * Implementation of this interface should provide whichever is more 
     * appropriate and should also return something if either one is available. 
     *
     * @return string|NULL Locality name or NULL if none is set
     */
    public function getLocality()
    {
        return $this->locality;
    }

    /**
     * Returns the country 2-letter ISO code
     * 
     * @return string|NULL 2-letter ISO country code or NULL if none is set
     */
    public function getCountryCode()
    {
        return $this->countryCode;
    }

    /**
     * Returns the region code
     * 
     * Region can be state/province/etc...
     * 
     * @return string|NULL Region code or NULL if none is set
     */
    public function getRegionCode()
    {
        return $this->regionCode;
    }

    /**
     * Returns the sub region information 
     * 
     * Sub-region can be county/etc... This is of course locale-dependent
     * 
     * @return string|NULL Sub-region information or NULL if not-applicable 
     *                     or none is set
     */
    public function getSubRegion()
    {
        return $this->subRegion;
    }

    /**
     * Returns a geodetic instance matching the location
     * 
     * @return Structures_Location_GeodeticInterface|NULL Instance or NULL if 
     *                                                    none is set
     */
    public function getGeodetic()
    {
        return $this->geodetic;
    }

    /**
     * Sets the "street" address
     * 
     * "Street" is used loosely to represent any thoroughfare, place, etc...
     * A street address is usually composed of:
     * <ul>
     *  <li>thoroughfare type (i.e. avenue, boulevard, plaza, ...)</li>
     *  <li>thoroughfare number (not necessarily digits only)</li>
     *  <li>thoroughfare name</li>
     * </ul>
     * The order of these elements depends on the locale of the country.
     * Additional location information such as apartment, building, dock, unit
     * and so on should be provided using setSecondaryUnit().
     * 
     * @param string $address "Street" address
     * 
     * @return void
     */
    public function setStreetAddress($address)
    {
        $address = trim($address);
        $this->streetAddress = (strlen($address)) ? $address : null;
    }

    /**
     * Sets the address line for apartment, suite, lot, unit, etc
     * 
     * @param string $unit Secondary "unit" information
     * 
     * @return void
     */
    public function setSecondaryUnit($unit)
    {
        $unit = trim($unit);
        $this->secondaryUnit = (strlen($unit)) ? $unit : null;
    }

    /**
     * Sets the locality
     * 
     * This is often useful for rural locations where mail is delivered to the
     * closest town. Postal office and locality are often used interchangeably.
     *
     * @param string $name Locality name
     * 
     * @return void
     */
    public function setLocality($name)
    {
        $name = trim($name);
        $this->locality = (strlen($name)) ? $name : null;
    }

    /**
     * Checks and sets the geodetic instance
     *
     * @param Structures_Location_GeodeticInterface $geo instance
     * 
     * @return void
     * @throws Structures_Location_Exception_Invalid
     */
    public function setGeodetic($geo)
    {
        if ($geo !== null 
            && !($geo instanceof Structures_Location_GeodeticInterface)) {
            throw new Structures_Location_Exception_Invalid('Not a valid instance');
        }
        $this->geodetic = $geo;
    }

    /**
     * Checks and sets the country code
     *
     * @param string $code 2-letter code
     * 
     * @return void
     * @throws Structures_Location_Exception_Invalid
     */
    public function setCountryCode($code)
    {
        if ($code !== null) {
            $code = self::cleanCountryCode($code);
        }
        $this->countryCode = $code;
    }

    /**
     * ASCII to-upper
     *
     * This method is to circumvent the issue of strtoupper('i') when
     * locale is Turkish.
     * 
     * @param string $string
     * 
     * @return string Uppercased string (Turkish compliant)
     */
    protected static function _toUpper($string)
    {
        return strtr($string, 
                     'abcdefghijklmnopqrstuvwxyz', 
                     'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
    }

    /**
     * Cleans up a country code
     *
     * @param string $code 2-letter ISO country code
     * 
     * @return string 2-letter ISO country code
     */
    protected static function cleanCountryCode($code)
    {
        static $i18n = null;
        if ($i18n === null) {
            $i18n = new I18Nv2_Country();
            $i18n->loadLanguage('en');
        }
        $code = self::_toUpper($code);
        if (!$i18n->isValidCode($code)) {
            throw new Structures_Location_Exception_Invalid($code, 'COUNTRY_CODE');
        }
        return $code;
    }

    /**
     * Returns an address parser for the country
     *
     * @param string|Structures_LocationInterface $countryCode 2-letter ISO country code or Structure Location instance
     * 
     * @return Structures_Location_ParserInterface
     * @throws Structures_Location_Exception_Incomplete
     * @throws Structures_Location_Exception_NotFound
     * @throws Structures_Location_Exception_Invalid
     */
    public static function getParser($countryCode)
    {
        static $parsers = array();

        if ($countryCode instanceof Structures_LocationInterface) {
            $code = $countryCode->getCountryCode();
            if ($code === null) {
                throw new Structures_Location_Exception_Incomplete('Missing country information', 'COUNTRY_CODE');
            }
        } else {
            $code = self::cleanCountryCode($countryCode);
        }

        if (!isset($parsers[$code])) {            
            $class = 'Structures_Location_'.$code.'_Parser';
            $file  = 'Structures/Location/'.$code.'/Parser.php';

            $fp = @fopen($file, 'r', true);
            @fclose($fp);
            if ($fp === false) {
                throw new Structures_Location_Exception_NotFound('No parser found for country '.$code);
            }
            include_once $file;
            $instance = new $class();
            if (!($instance instanceof Structures_Location_ParserInterface)) {
                throw new Structures_Location_Exception_Invalid('Parser instance is not a '.Structures_Location_ParserInterface);
            }
            $parsers[$code] = $instance;
        }
        return $parsers[$code];
    }
    
    /**
     * Blanks an object's location properties
     * 
     * Assigns NULL to all properties of the location relating to the interface
     *
     * @param Structures_LocationInterface $object
     * 
     * @return void
     */
    static public function blank(Structures_LocationInterface $object)
    {
        $object->setCountryCode(null);
        $object->setRegionCode(null);
        $object->setSubRegion(null);
        $object->setLocality(null);
        $object->setSecondaryUnit(null);
        $object->setStreetAddress(null);
        $object->setGeodetic(null);
    }
}

?>